一文彻底搞懂多线程、高并发原理和知识点

您所在的位置:网站首页 java 并发处理数据 一文彻底搞懂多线程、高并发原理和知识点

一文彻底搞懂多线程、高并发原理和知识点

2024-07-10 19:26| 来源: 网络整理| 查看: 265

目录一、多线程理论1.1、操作系统的发展1.1.1、批处理操作系统1.1.2、如何提高CPU利用率1.1.3、进程来了1.2、并发和并行1.2.1、并发1.2.2、并行1.3、线程1.3.1、线程出现的原因1.3.2、线程1.3.3、线程工作的原理1.4、线程和进程的区别1.5、线程调度1.5.1、分时调度1.5.2、抢占式调度二、实现线程的方式2.1、继承Thread类2.1.1、继承Thread类的优点2.1.2、继承Thread类的缺点2.2、实现 Runnable 接口2.2.1、实现Runnable的接口的优点2.3、实现Callable接口(拓展)2.3.1、实现Callable接口优点2.4、两种实现方式的区别2.4.1、Thread实现2.4.2、Runable实现2.4.3、两者实现的区别2.5、存在的问题2.6、多线程执行轨迹分析2.7、总结三、线程常用的方法3.1、设置线程优先级3.2、线程的强制执行3.3、线程休眠3.3.1、正常执行3.3.2、异常情况3.3.3、总结3.4、线程的礼让3.5、线程结束四、线程生命周期(状态)4.1、新生状态4.2、就绪状态4.3、运行状态4.4、阻塞状态4.5、死亡状态五、线程安全问题5.1、问题引入5.2、同步操作5.3、同步代码块5.3.1、同步监视器5.3.2、总结5.4、同步方法5.5、Lock锁5.6、线程通信5.6.1、线程通信总结5.7、总结六、 volatile关键字6.1、问题引入6.2、多线程下变量的不可见性6.2.1、概述6.2.2、问题分析6.2.3、多线程下变量的不可见性的原因6.2.4、解决方案6.2.4.1、加锁6.2.4.2、对共享变量使用volatile关键字修饰6.2.5、volatile和synchronized七、原子性7.1、问题引入7.2、问题原理说明7.3、volition的原子性验证7.4、问题解决办法7.4.1、使用锁机制(加锁)7.4.2、使用原子类7.4.2.1、概述7.4.2.2、常用API7.5、原子类CAS机制7.5.1、CAS机制详解7.6、乐观锁和悲观锁7.6.1、悲观锁7.6.2、乐观锁八、并发包8.1、ConcurrentHashMap8.1.1、HashMap线程不安全演示8.1.2、Hashtable演示8.1.3、ConcurrentHashMap演示8.1.4、HashTable效率低下的原因8.1.5、ConcurrentHashMap高效的原因8.1.6、总结8.2、CountDownLatch8.2.1、例子8.2.2、总结8.3、CyclicBarrier8.3.1、例子8.3.2、使用场景8.4、Semaphore8.4.1、示范一8.4.2、示范二8.5、Exchanger8.5.1、exchange方法的阻塞特性8.5.2、exchange方法执行交换8.5.3、exchange方法超时8.5.4、使用场景九、线程池9.1、线程池的概念9.2、线程池的实现9.2.1、Runnable9.2.2、Callable十、死锁10.1、死锁产生的必要条件10.2、死锁代码实现

一、多线程理论 1.1、操作系统的发展

​ 在计算机发明之前,人们处理大量的计算是通过人工处理的,耗费人力,成本很大而且错误较多。为了处理大量的数学计算问题,人们发明了计算机。

​ 最初的计算机只能接受一些特定的指令,用户输入一个指令,计算机就做出一个操作。当用户在思考或者输入时,计算机就在等待。显然这样效率低下,在很多时候,计算机都处在等待状态。

1.1.1、批处理操作系统

​ 既然传统计算机那么慢,那么能不能把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机,计算机通过不断得读取指令进行相应的操作。

​ 就这样,批处理操作系统诞生了。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。

1.1.2、如何提高CPU利用率

​ 虽然批处理操作系统的诞生提高了任务处理的便捷性(省略了用户输入的时间),但是仍然存在一个很大的问题:

​ 假如有两个任务A和B,需要读取大量的数据输入(I/O操作),而其实CPU只能处在等待状态,等任务A读取完数据再能继续进行,这样就白白浪费了CPU资源。于是人们就想,能否在任务A读取数据的过程中,让任务B去执行,当任务A读取完数据之后,暂停任务B,让任务A继续执行?

​ 这时候又出现了几个问题:内存中始终都只有一个程序在运行,而想要解决上述问题,必然要在内存中装入多个程序,如何处理呢?多个程序使用的数据如何辨别?当一个程序暂停后,随后怎么恢复到它之前执行的状态呢?

1.1.3、进程来了

​ 这时候,人们就发明了进程,用一个进程对应一个程序,每个进程都对应一定的内存地址和内存空间,并且只能自己使用自己的内存空间,多个进程之间的内存互不共享,且进程之间彼此不打扰。

​ 进程同时也保存了程序每时每刻的运行状态,为进程切换提供了如可能。

​ 当进程暂停时,它会保存当前进程的状态(进程标识,进程使用的资源等),在下一次切换回来时根据之前保存的2状态进行恢复,接着继续执行。

1.2、并发和并行 1.2.1、并发

​ 并发是能够让操作系统从宏观上看起来同一时间段执行多个任务。 换句话说,进程让操作体统的并发成为了可能,至此出现多任务操作系统。

​ 虽然并发从宏观上看是有多个任务在执行,但是实际上对于单核CPU来说,任意具体时刻都只有一个任务在占用CPU资源,操作系统一般通过CPU时间片轮转来实现并发。

​ 总的来说,并发就是在一段时间内多个进程轮流使用同一个 CPU,多个进程形成并发。

image-20201220125013715

1.2.2、并行

​ 在同一时刻多个进程使用各自的 CPU,多个进程形成并行。并行需要多个 CPU 支持。

并行与并发

1.3、线程 1.3.1、线程出现的原因

​ 出现了进程之后,操作系统的性能(CPU利用率)得到了大大的提升。虽然进程的出现解决了操作系统的并发问题,但是人们不满足,逐渐对实时性有了要求。因为一个进程在一个时间段内只能做一个事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。

​ 举一个例子:对于监控系统这个进程来说,不仅要与服务器端进行通信获取图像数据并将图像信息显示在画面上,还要处理与用户的交互操作。如果在一个时刻该系统正在与服务器通信获取图像数据,而用户在监控系统上点击了一个按钮,那么系统只能等获取完图像后才能与用户进行交互操作。如果获取图像需要10s,用户就得等待10s。显然这样的系统,无法满足人们的需求。

1.3.2、线程

​ 为了让子任务可以分开执行,即上个例子说的,在与服务器通信获取图形数据的同时相应用户,为了处理这种情况,人们发明了线程,一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。在用户点击按钮的时候,可以暂停获取图像数据的线程,让出CPU资源,让UI线程获取CPU资源,响应用户的操作,响应完后再切换回来,获取图像数据的线程重新获取CPU资源。让用户感觉系统在同时做很多事,满足用户对实时性的要求。线程的出现是为了解决实时性的问题

​ 总的来说,线程是进程的细分,通常,在实时性操作系统中,进程会被划分为多个可以独立运行的子任务,这些子任务被称为线程,多个线程配合完成一个进程的任务。

注意

​ 一个进程包含多个线程,但是这些线程共享进程占有的内存地址空间和资源。进程是操作系统进行资源分配的基本单位(进程之间互不干扰),而线程是操作系统进行CPU调度的基本单位(线程间互相切换)。

image-20201220131319289

1.3.3、线程工作的原理

​ 假设 P 进程抢占 CPU 后开始执行,此时如果 P 进行正在进行获取网络资源的操作时,用户进行UI 操作,此时 P 进程不会响应 UI 操作。可以把 P 进程可以分为 Ta、Tb 两个线程。Ta 用于获取网络资源,Tb 用于响应 UI 操作。此时如果 Ta 正在执行获取网络资源时、用户进行 UI 操作,为了做到实时性,Ta 线程暂时挂起,Tb 抢占 CPU 资源,执行 UI 操作,UI 操作执行完成后让出CPU,Ta 抢占 CPU 资源继续执行请求网络资源。

总结

线程再一次提高了CPU的利用率 线程是包含在进程中,是对进程任务的细分,线程共享进程资源(内存资源等) 线程细分后称为 CPU 调度的基本单位。进程称为操作系统资源分配的基本单位。 1.4、线程和进程的区别 根本区别:进程是操作系统资源分配的基本单位,而线程是CPU调度和执行的基本单位 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行) 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分。 1.5、线程调度 1.5.1、分时调度

​ 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

1.5.2、抢占式调度

​ 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

二、实现线程的方式

​ 在 Java 中实现线程的方式有 2 种,一种是继承 Thread,一种是实现 Runnable 接口。

​ 如果一个进程没有任何线程,我们成为单线程应用程序;如果一个进程有多个线程存在,我们成为多线程应用程序。进程执行时一定会有一个主线程(main 线程)存在,主线程有能力创建其他线程。多个线程抢占 CPU,导致程序的运行轨迹不确定。多线程的运行结果也不确定。

2.1、继承Thread类

​ 线程开启我们需要用到了java.lang.Thread类,API中该类中定义了有关线程的一些方法,具体如下:

构造方法

public Thread():分配一个新的线程对象。 public Thread(String name):分配一个指定名字的新的线程对象。 public Thread(Runnable target):分配一个带有指定目标新的线程对象。 public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。

常用方法

public String getName():获取当前线程名称。 public void start():导致此线程开始执行; Java虚拟机调用此线程的run方法。 public void run():此线程要执行的任务在此处定义代码。 public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。 public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

​ 继承 Thread 实现多线程,必须重写 run 方法,启动的时候调用的也是调用线程对象的start()方法来启动该线程,如果直接调用run()方法的话,相当于普通类的执行,此时相当于只有主线程在执行。

package day16_thread.classing.thread; /** * @author Xiao_Lin * @date 2020/12/20 11:40 */ public class MyThread extends Thread{ @Override public void run() { for (int i =1;i 0){ // 3.账户有钱,有钱可以取 this.money -= money; System.out.println(name+"来取钱"+money+"取钱后剩余:"+this.money); // 4.没钱,先唤醒别人,等待自己,。 this.notifyAll(); this.wait(); }else{ // 5.余额不足,没钱,先唤醒别人,等待自己,。 this.notifyAll(); this.wait(); } }catch (Exception e){ e.printStackTrace(); } } public String getCardId() { return cardId; } public void setCardId(String cardId) { this.cardId = cardId; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } } package com; /** 取钱的线程类 */ public class DrawThread extends Thread { private Account acc ; // 定义了一个账户类型的成员变量接收取款的账户对象! public DrawThread(Account acc , String name){ super(name); // 为当前线程对象取名字 this.acc = acc ; } @Override public void run() { while(true){ try { Thread.sleep(4000); acc.drawMoney(10000); } catch (Exception e) { e.printStackTrace(); } } } } package com; /** 存钱的线程类 */ public class SaveThread extends Thread { private Account acc ; // 定义了一个账户类型的成员变量接收取款的账户对象! public SaveThread(Account acc , String name){ super(name); // 为当前线程对象取名字 this.acc = acc ; } @Override public void run() { while(true){ try { Thread.sleep(4000); acc.saveMoney(10000); } catch (Exception e) { e.printStackTrace(); } } } } package com; /** 目标:线程通信(了解原理,代码几乎不用) 线程通信:多个线程因为在同一个进程中,所以互相通信比较容易的。 线程通信的经典模型:生产者与消费者问题。 生产者负责生成商品,消费者负责消费商品。 生产不能过剩,消费不能没有。 模拟一个案例: 小明和小红有一个共同账户:共享资源 他们有3个爸爸(亲爸,岳父,干爹)给他们存钱。 模型:小明和小红去取钱,如果有钱就取出,然后等待自己,唤醒他们3个爸爸们来存钱 他们的爸爸们来存钱,如果发现有钱就不存,没钱就存钱,然后等待自己,唤醒孩子们来取钱。 做整存整取:10000元。 分析: 生产者线程:亲爸,岳父,干爹 消费者线程:小明,小红 共享资源:账户对象。 注意:线程通信一定是多个线程在操作同一个资源才需要进行通信。 线程通信必须先保证线程安全,否则毫无意义,代码也会报错! 线程通信的核心方法: public void wait(): 让当前线程进入到等待状态 此方法必须锁对象调用. public void notify() : 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用 public void notifyAll() : 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用 小结: 是一种等待唤醒机制。 必须是在同一个共享资源才需要通信,而且必须保证线程安全。 */ public class ThreadCommunication { public static void main(String[] args) { // 1.创建一个账户对象。 Account acc = new Account("ICBC-1313113",0); // 2.创建2个取钱线程。 new DrawThread(acc , "小明").start(); new DrawThread(acc , "小红").start(); // 3.创建3个存钱线程。 new SaveThread(acc , "亲爹").start(); new SaveThread(acc , "干爹").start(); new SaveThread(acc , "岳父").start(); } } 5.6.1、线程通信总结 线程通信是一种等待唤醒机制。 线程安全必须早同一个共享资源才需要通信,而且必须保证线程安全。 5.7、总结 线程安全,性能差。 线程不安全性能好,假如开发中不会存在多线程的安全问题,建议使用线程不安全的设计类。 六、 volatile关键字 6.1、问题引入 public class VolatileThread extends Thread { // 定义成员变量 private boolean flag = false ; public boolean isFlag() { return flag;} @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 将flag的值更改为true this.flag = true ; System.out.println("flag=" + flag); } } public class VolatileThreadDemo {// 测试类 public static void main(String[] args) { // 创建VolatileThread线程对象 VolatileThread volatileThread = new VolatileThread() ; volatileThread.start(); // main方法 while(true) { if(volatileThread.isFlag()) { System.out.println("执行了======"); } } } } 6.2、多线程下变量的不可见性 6.2.1、概述

​ 在介绍多线程并发修改变量不可见现象的原因之前,我们先看看另一种Java内存模型(和Java并发编程有关的模型):JMM。

​ JMM(Java Memory Model):Java内存模型是Java虚拟机规范中定义的一种内存模型,Java内存模型是标准化的,他屏蔽了底层不同计算机的硬件的不同

​ Java内存模型描述了Java程序中各种变量(线程共享变量)的访问规则以及在JVM中将变量存储到内存和从内存中读取变量的底层细节。

​ JMM有以下规定:

所有的共享变量都存储于主内存(这里的变量是指实例变量和类变量,不包含局部变量,因为局部变量的线程是私有的,不存在竞争的问题) 每一个线程都有自己独立的工作内存,线程的工作内存保留了被线程使用的变量的工作副本 线程对变量的所有操作(读、取)都必须在工作内存中完成,而不能直接读写主内存的变量。

本地内存和主内存之间的关系:

image-20201231120757336

6.2.2、问题分析

JMM中主内存和本地内存-第 2 页

子线程1从主内存中读取到数据并复制到其对应的工作内存。 修改flag的值为true,但是这个时候flag的值还并没有写会主内存。 此时main方法读取到了flag的值为false。 当子线程1将flag的值写回去之后,由于main函数中的while(true)调用的是系统底层的代码,速度快,快到没有时间再去读取主内存中的值,所以此时while(true)读取到的值一直是flag = false。 此时我们能想到的办法是,如果main线程从主内存中读取到了flag最新的值,那么if语句就可以执行了。 6.2.3、多线程下变量的不可见性的原因 每个线程都有自己的工作内存,线程都是从主内存中拷贝到共享变量的副本值 每个线程都是在自己的工作内存中操作共享变量的。 6.2.4、解决方案 6.2.4.1、加锁 while(true){ synchronized(t){ if(t.isFlag()){ System.out.print("主线程进入循环") } } }

​ 第一个线程进入synchronized代码块前后,执行过程如下:

线程获得锁 清空工作内存 从主内存中拷贝共享变量的最新值变成副本 执行代码 将修改后的值重新放回主内存中 线程释放锁 6.2.4.2、对共享变量使用volatile关键字修饰

​ 我们还可以对共享变量用volatile关键字修饰,volatile关键字的作用是在多线程并发下修改共享变量实现可见性。,一旦一线程修改了volatile修饰的变量,另一个线程可以立即读取到最新值。

JMM中主内存和本地内存-第 3 页

6.2.5、volatile和synchronized volatile只能修饰实例变量和类变量,而synchronized可以修饰方法以及代码块 volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全),而synchronized是一种排他互斥的机制,可以保证线程安全。 七、原子性

​ 所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。

7.1、问题引入 public class VolatileAtomicThread implements Runnable { // 定义一个int类型的遍历 private int count = 0 ; @Override public void run() { // 对该变量进行++操作,100次 for(int x = 0 ; x < 100 ; x++) { count++ ; System.out.println("count =========>>>> " + count); } } } public class VolatileAtomicThreadDemo { public static void main(String[] args) { // 创建VolatileAtomicThread对象 VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ; // 开启100个线程对count进行++操作 for(int x = 0 ; x < 100 ; x++) { new Thread(volatileAtomicThread).start(); } } }

​ 执行结果:不保证一定是10000

7.2、问题原理说明

以上问题主要是发生在count++操作上,count++操作包含3个步骤:

从主内存中读取数据到工作内存 对工作内存中的数据进行++操作 将工作内存中的数据写回到主内存

​ count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。

1561524132215

1)假设此时x的值是100,线程A需要对改变量进行自增1的操作,首先它需要从主内存中读取变量x的值。由于CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态

2)线程B也需要从主内存中读取x变量的值,由于线程A没有对x值做任何修改因此此时B读取到的数据还是100

3)线程B工作内存中x执行了+1操作,但是未刷新之主内存中

4)此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效。A线程对工作内存中的数据进行了+1操作

5)线程B将101写入到主内存

6)线程A将101写入到主内存

虽然计算了2次,但是只对A进行了1次修改。

7.3、volition的原子性验证 // 定义一个int类型的变量 private volatile int count = 0 ;

​ 小结:在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。

​ 在多线程环境下,要保证数据的安全性,我们还需要使用锁机制。

7.4、问题解决办法 7.4.1、使用锁机制(加锁)

​ 我们可以给count++操作添加锁,那么count++操作就是临界区的代码,临界区只能有一个线程去执行,所以count++就变成了原子操作。

​ 缺点:性能差。

public class VolatileAtomicThread implements Runnable { // 定义一个int类型的变量 private volatile int count = 0 ; private static final Object obj = new Object(); @Override public void run() { // 对该变量进行++操作,100次 for(int x = 0 ; x < 100 ; x++) { synchronized (obj) { count++ ; System.out.println("count =========>>>> " + count); } } } } 7.4.2、使用原子类 7.4.2.1、概述

​ Java从JDK5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。我们可以使用原子类来保证原子性操作,从而保证线程安全。

7.4.2.2、常用API

​ 我们以Integer的原子类进行讲解。

方法 概述 public AtomicInteger(): 初始化一个默认值为0的原子型Integer public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer int get(): 获取值 int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。 int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。 int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。 int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。 public class VolatileAtomicThread implements Runnable { // 定义一个int类型的变量,默认值是0,我们也可以指定长度 private AtomicInteger atomicInteger = new AtomicInteger() ; @Override public void run() { // 对该变量进行++操作,100次 for(int x = 0 ; x < 100 ; x++) { int i = atomicInteger.getAndIncrement(); System.out.println("count =========>>>> " + i); } } } 7.5、原子类CAS机制

​ CAS的全成是: Compare And Swap(比较再交换); 是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS可以将read-modify-check-write转换为原子操作,这个原子操作直接由处理器保证。

​ CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

7.5.1、CAS机制详解 在内存地址V当中,存储着值为10的变量。

1561550710005

此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

1561550793084

在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

1561550911597

线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,说明值已经被更改过了,提交失败。

1561551122602

线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

1561551254968

这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的,说明并没有人修改过值。

1561551329313

线程1进行SWAP,把地址V的值替换为B,也就是12。

1561551377905

7.6、乐观锁和悲观锁

​ CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?

7.6.1、悲观锁

​ Synchronized是从悲观的角度出发,是一个典型的悲观锁。

​ 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

​ 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。性能较差!

7.6.2、乐观锁

​ CAS是从乐观的角度出发,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

​ CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!很多数据库都会使用到乐观锁机制。

八、并发包

​ 在实际开发中如果不需要考虑线程安全问题,大家不需要做线程安全,因为如果做了反而性能不好!但是开发中有很多业务是需要考虑线程安全问题的,此时就必须考虑了。否则业务出现问题。 ​ Java为很多业务场景提供了性能优异,且线程安全的并发包,程序员可以选择使用!

8.1、ConcurrentHashMap

​ Map集合中的经典集合:HashMap它是线程不安全的,性能好。如果在要求线程安全的业务情况下就不能用这个集合做Map集合,否则业务会崩溃。

​ 为了保证线程安全,可以使用Hashtable。Hashtable是线程安全的Map集合,但是性能较差!(已经被淘汰了,虽然安全,但是性能差)

​ 为什么说HashTable的性能差呢?我们看看源码可以得知,HashTable的每一个方法都用synchronized修饰了,实在是过于悲观。

​ ConcurrentHashMap不止线程安全,而且效率高,性能好,最新最好用的线程安全的Map集。

8.1.1、HashMap线程不安全演示 public class Const { public static HashMap map = new HashMap(); } public void run() { for (int i = 0; i < 500000; i++) { Const.map.put(this.getName() + (i + 1), this.getName() + i + 1); } System.out.println(this.getName() + " 结束!"); } public class Demo { public static void main(String[] args) throws InterruptedException { Thread1A a1 = new Thread1A(); Thread1A a2 = new Thread1A(); a1.setName("线程1-"); a2.setName("线程2-"); a1.start(); a2.start(); //休息10秒,确保两个线程执行完毕 Thread.sleep(1000 * 5); //打印集合大小 System.out.println("Map大小:" + Const.map.size()); } }

​ 我们执行后可以发现出来的错误是有以下三种:

没有达到预期的效果

aba6

抛出异常

HashMap异常

结果错误

HashMap错误结果

8.1.2、Hashtable演示 public class Const { public static Hashtable map = new Hashtable(); } public void run() { long start = System.currentTimeMillis(); for (int i = 0; i < 500000; i++) { Const.map.put(this.getName() + (i + 1), this.getName() + i + 1); } long end = System.currentTimeMillis(); System.out.println(this.getName() + " 结束!用时:" + (end - start) + " 毫秒"); } public class Demo { public static void main(String[] args) throws InterruptedException { Thread1A a1 = new Thread1A(); Thread1A a2 = new Thread1A(); a1.setName("线程1-"); a2.setName("线程2-"); a1.start(); a2.start(); //休息10秒,确保两个线程执行完毕 Thread.sleep(1000 * 5); //打印集合大小 System.out.println("Map大小:" + Const.map.size()); } }

Hashtable执行时间

8.1.3、ConcurrentHashMap演示 public class Const { public static ConcurrentHashMap map = new ConcurrentHashMap(); } public void run() { long start = System.currentTimeMillis(); for (int i = 0; i < 500000; i++) { Const.map.put(this.getName() + (i + 1), this.getName() + i + 1); } long end = System.currentTimeMillis(); System.out.println(this.getName() + " 结束!用时:" + (end - start) + " 毫秒"); } public class Demo { public static void main(String[] args) throws InterruptedException { Thread1A a1 = new Thread1A(); Thread1A a2 = new Thread1A(); a1.setName("线程1-"); a2.setName("线程2-"); a1.start(); a2.start(); //休息10秒,确保两个线程执行完毕 Thread.sleep(1000 * 5); //打印集合大小 System.out.println("Map大小:" + Const.map.size()); } }

ConcurrentHashMap执行时间

8.1.4、HashTable效率低下的原因

​ 我们查看HashTable的源码我们可以发现他的每一个方法都用synchronized修饰了,实在是过于悲观。

public synchronized V put(K key, V value) public synchronized V get(Object key)

​ 在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

Hashtable锁示意图

8.1.5、ConcurrentHashMap高效的原因

​ ConcurrentHashMap上锁机制:CAS + 局部(synchronized)锁定(分段式锁)

ConcurrentHashMap锁示意图

8.1.6、总结 HashMap是线程不安全的。 ​ Hashtable线程安全基于synchronized,综合性能差,被淘汰了。 ​ ConcurrentHashMap:线程安全的,分段式锁,综合性能最好,线程安全开发中推荐使用 8.2、CountDownLatch

​ CountDownLatch允许一个或多个线程等待其他线程完成操作,再执行自己。

需求

​ 线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。

CountDownLatch构造器以及方法

public CountDownLatch(int count)// 初始化唤醒需要的down几步。count相当于一个计数器 方法 详解 public void await() throws InterruptedException 让当前线程等待,必须down完初始化的数字才可以被唤醒,否则进入无限等待 public void countDown() 计数器进行减1 (down 1) 8.2.1、例子 public class ThreadA extends Thread { private CountDownLatch down ; public ThreadA(CountDownLatch down) { this.down = down; } @Override public void run() { System.out.println("A"); try { down.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("C"); } } public class ThreadB extends Thread { private CountDownLatch down ; public ThreadB(CountDownLatch down) { this.down = down; } @Override public void run() { System.out.println("B"); down.countDown(); } } public class Demo { public static void main(String[] args) { CountDownLatch down = new CountDownLatch(1);//创建1个计数器 new ThreadA(down).start(); new ThreadB(down).start(); } } 8.2.2、总结 CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。 CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch的wait()方法的线程阻塞状态解除,继续执行。 8.3、CyclicBarrier

​ CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。这里的屏障相当于需要达到的要求。

​ CyclicBarrier构造方法:

public CyclicBarrier(int parties, Runnable barrierAction)// 用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景

​ CyclicBarrier重要方法:

public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞 8.3.1、例子

制作员工线程

public class PersonThread extends Thread { private CyclicBarrier cbRef; public PersonThread(CyclicBarrier cbRef) { this.cbRef = cbRef; } @Override public void run() { try { Thread.sleep((int) (Math.random() * 1000)); System.out.println(Thread.currentThread().getName() + " 到了! "); cbRef.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }

制作开会线程

public class MeetingThread extends Thread { @Override public void run() { System.out.println("好了,人都到了,开始开会......"); } }

制作测试类

public class Demo { public static void main(String[] args) { CyclicBarrier cbRef = new CyclicBarrier(5, new MeetingThread());//等待5个线程执行完毕,再执行MeetingThread PersonThread p1 = new PersonThread(cbRef); PersonThread p2 = new PersonThread(cbRef); PersonThread p3 = new PersonThread(cbRef); PersonThread p4 = new PersonThread(cbRef); PersonThread p5 = new PersonThread(cbRef); p1.start(); p2.start(); p3.start(); p4.start(); p5.start(); } } 8.3.2、使用场景

​ CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。例如:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。

8.4、Semaphore

​ Semaphore(发信号)的主要作用是控制线程的并发数量。他的机制和synchronized一样都是上锁,但是,但某个时间段内,synchronized只能有一个线程允许执行。Semaphore可以设置同时允许几个线程执行。它的作用是控制访问特定资源的线程数目。

​ Semaphore构造方法:

public Semaphore(int permits) //permits 表示许可线程的数量 public Semaphore(int permits, boolean fair) //fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程

Semaphore重要方法:

public void acquire() throws InterruptedException //表示获取许可 public void release() //release() 表示释放许可 8.4.1、示范一

​ 我们测试一下只允许一个线程的案例。

制作一个Service类

public class Service { private Semaphore semaphore = new Semaphore(1);//1表示许可的意思,表示最多允许1个线程执行acquire()和release()之间的内容 public void testMethod() { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + " 进入 时间=" + System.currentTimeMillis()); Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + " 结束 时间=" + System.currentTimeMillis()); semaphore.release(); //acquire()和release()方法之间的代码为"同步代码" } catch (InterruptedException e) { e.printStackTrace(); } } }

制作线程类

public class ThreadA extends Thread { private Service service; public ThreadA(Service service) { super(); this.service = service; } @Override public void run() { service.testMethod(); } }

测试类

public class Demo { public static void main(String[] args) { Service service = new Service(); //启动5个线程 for (int i = 1; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3